<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <title>插件开发 - 为企业级框架和应用而生</title>
  <meta charset="utf-8">
  <meta name="description" content="index.description">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/images/favicon.png" type="image/x-icon">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css" />
<link rel="stylesheet" href="/css/index.css">

    <script>
    !function(t,e,a,r,c){t.TracertCmdCache=t.TracertCmdCache||[],t[c]=window[c]||
      {_isInit:!0,call:function(){t.TracertCmdCache.push(arguments)},
      start:function(t){this.call('start',t)}},t[c].l=new Date;
      var n=e.createElement(a),s=e.getElementsByTagName(a)[0];
      n.async=!0,n.src=r,s.parentNode.insertBefore(n,s)}
    (window,document,'script','https://tracert.alipay.com/tracert.js','Tracert');
      Tracert.start({
        plugins: [ 'BucName' ],
        spmAPos: 'a454',
        spmBPos: 'b4893',
      });
    </script>
  
</head>
<body>
  <div class="nav" >
  <header>
    <a href="/zh-cn/" class="nav-logo leftpadding" alt="egg"><img src="https://zos.alipayobjects.com/rmsportal/VTcUYAaoKqXyHJbLAPyF.svg"></a>
    <ul class="nav-item">
      <li>
        <form id="search-form">
          <input type="text" id="search-query" class="search-query st-default-search-input">
        </form>
      </li>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/advanced/plugin.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/advanced/plugin.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
      <li><iframe src="https://ghbtns.com/github-btn.html?user=eggjs&repo=egg&type=star&count=true" frameborder="0" scrolling="0" width="150px" height="20px"></iframe></li>
    </ul>
    <a id="mobileTrigger" href="#" class="mobile-trigger">
      <ul>
        <li></li>
        <li></li>
        <li></li>
      </ul>
    </a>
  </header>
</div>
  <div id="container" class="container">
    <div class="page-main">
  <article class="markdown-body">
    <h1>插件开发</h1>
    <p>插件机制是我们框架的一大特色。它不但可以保证框架核心的足够精简、稳定、高效，还可以促进业务逻辑的复用，生态圈的形成。有人可能会问了</p>
<ul>
<li>Koa 已经有了中间件的机制，为啥还要插件呢？</li>
<li>中间件、插件、应用它们之间是什么关系，有什么区别？</li>
<li>我该怎么使用一个插件？</li>
<li>如何编写一个插件？</li>
<li>...</li>
</ul>
<p>在<a href="../basics/plugin.html">使用插件</a>章节我们已经讨论过前几点，接下来我们来看看如何开发一个插件。</p>
<h2 id="插件开发"><a class="markdown-anchor" href="#插件开发">#</a> 插件开发</h2>
<h3 id="使用脚手架快速开发"><a class="markdown-anchor" href="#使用脚手架快速开发">#</a> 使用脚手架快速开发</h3>
<p>你可以直接通过 <a href="https://github.com/eggjs/egg-init" target="_blank" rel="noopener">egg-init</a> 选择 <a href="https://github.com/eggjs/egg-boilerplate-plugin" target="_blank" rel="noopener">plugin</a> 脚手架来快速上手。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ egg-init --<span class="built_in">type</span>=plugin egg-hello</span><br><span class="line">$ <span class="built_in">cd</span> egg-hello</span><br><span class="line">$ npm i</span><br><span class="line">$ npm <span class="built_in">test</span></span><br></pre></td></tr></table></figure>
<h2 id="插件的目录结构"><a class="markdown-anchor" href="#插件的目录结构">#</a> 插件的目录结构</h2>
<p>一个插件其实就是一个『迷你的应用』，下面展示的是一个插件的目录结构，和应用（app）几乎一样。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">. egg-hello</span><br><span class="line">├── package.json</span><br><span class="line">├── app.js (可选)</span><br><span class="line">├── agent.js (可选)</span><br><span class="line">├── app</span><br><span class="line">│   ├── extend (可选)</span><br><span class="line">│   |   ├── helper.js (可选)</span><br><span class="line">│   |   ├── request.js (可选)</span><br><span class="line">│   |   ├── response.js (可选)</span><br><span class="line">│   |   ├── context.js (可选)</span><br><span class="line">│   |   ├── application.js (可选)</span><br><span class="line">│   |   └── agent.js (可选)</span><br><span class="line">│   ├── service (可选)</span><br><span class="line">│   └── middleware (可选)</span><br><span class="line">│       └── mw.js</span><br><span class="line">├── config</span><br><span class="line">|   ├── config.default.js</span><br><span class="line">│   ├── config.prod.js</span><br><span class="line">|   ├── config.test.js (可选)</span><br><span class="line">|   ├── config.local.js (可选)</span><br><span class="line">|   └── config.unittest.js (可选)</span><br><span class="line">└── test</span><br><span class="line">    └── middleware</span><br><span class="line">        └── mw.test.js</span><br></pre></td></tr></table></figure>
<p>那区别在哪儿呢？</p>
<ol>
<li>
<p>插件没有独立的 router 和 controller。这主要出于几点考虑：</p>
<ul>
<li>路由一般和应用强绑定的，不具备通用性。</li>
<li>一个应用可能依赖很多个插件，如果插件支持路由可能导致路由冲突。</li>
<li>如果确实有统一路由的需求，可以考虑在插件里通过中间件来实现。</li>
</ul>
</li>
<li>
<p>插件需要在 <code>package.json</code> 中的 <code>eggPlugin</code> 节点指定插件特有的信息：</p>
<ul>
<li>
<p><code>{String} name</code> - 插件名（必须配置），具有唯一性，配置依赖关系时会指定依赖插件的 name。</p>
</li>
<li>
<p><code>{Array} dependencies</code> - 当前插件强依赖的插件列表（如果依赖的插件没找到，应用启动失败）。</p>
</li>
<li>
<p><code>{Array} optionalDependencies</code> - 当前插件的可选依赖插件列表（如果依赖的插件未开启，只会 warning，不会影响应用启动）。</p>
</li>
<li>
<p><code>{Array} env</code> - 只有在指定运行环境才能开启，具体有哪些环境可以参考<a href="../basics/env.html">运行环境</a>。此配置是可选的，一般情况下都不需要配置。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"name"</span>: <span class="string">"egg-rpc"</span>,</span><br><span class="line">  <span class="attr">"eggPlugin"</span>: &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"rpc"</span>,</span><br><span class="line">    <span class="attr">"dependencies"</span>: [ <span class="string">"registry"</span> ],</span><br><span class="line">    <span class="attr">"optionalDependencies"</span>: [ <span class="string">"vip"</span> ],</span><br><span class="line">    <span class="attr">"env"</span>: [ <span class="string">"local"</span>, <span class="string">"test"</span>, <span class="string">"unittest"</span>, <span class="string">"prod"</span> ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
</ul>
</li>
<li>
<p>插件没有 <code>plugin.js</code>：</p>
<ul>
<li><code>eggPlugin.dependencies</code> 只是用于声明依赖关系，而不是引入插件或开启插件。</li>
<li>如果期望统一管理多个插件的开启和配置，可以在<a href="./framework.html">上层框架</a>处理。</li>
</ul>
</li>
</ol>
<h2 id="插件的依赖管理"><a class="markdown-anchor" href="#插件的依赖管理">#</a> 插件的依赖管理</h2>
<p>和中间件不同，插件是自己管理依赖的。应用在加载所有插件前会预先从它们的 <code>package.json</code> 中读取 <code>eggPlugin &gt; dependencies</code> 和 <code>eggPlugin &gt; optionalDependencies</code> 节点，然后根据依赖关系计算出加载顺序，举个例子，下面三个插件的加载顺序就应该是 <code>c =&gt; b =&gt; a</code></p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line">// plugin a</span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">"name"</span>: <span class="string">"egg-plugin-a"</span>,</span><br><span class="line">  <span class="attr">"eggPlugin"</span>: &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"a"</span>,</span><br><span class="line">    <span class="attr">"dependencies"</span>: [ <span class="string">"b"</span> ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// plugin b</span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">"name"</span>: <span class="string">"egg-plugin-b"</span>,</span><br><span class="line">  <span class="attr">"eggPlugin"</span>: &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"b"</span>,</span><br><span class="line">    <span class="attr">"optionalDependencies"</span>: [ <span class="string">"c"</span> ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// plugin c</span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">"name"</span>: <span class="string">"egg-plugin-c"</span>,</span><br><span class="line">  <span class="attr">"eggPlugin"</span>: &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"c"</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><strong>注意：<code>dependencies</code> 和 <code>optionalDependencies</code> 的取值是另一个插件的 <code>eggPlugin.name</code>，而不是 <code>package name</code>。</strong></p>
<p><code>dependencies</code> 和 <code>optionalDependencies</code> 是从 <code>npm</code> 借鉴来的概念，大多数情况下我们都使用 <code>dependencies</code>，这也是我们最推荐的依赖方式。那什么时候可以用 <code>optionalDependencies</code> 呢？大致就两种：</p>
<ul>
<li>只在某些环境下才依赖，比如：一个鉴权插件，只在开发环境依赖一个 mock 数据的插件</li>
<li>弱依赖，比如：A 依赖 B，但是如果没有 B，A 有相应的降级方案</li>
</ul>
<p>需要特别强调的是：如果采用 <code>optionalDependencies</code> 那么框架不会校验依赖的插件是否开启，它的作用仅仅是计算加载顺序。所以，这时候依赖方需要通过『接口探测』等方式来决定相应的处理逻辑。</p>
<h2 id="插件能做什么"><a class="markdown-anchor" href="#插件能做什么">#</a> 插件能做什么？</h2>
<p>上面给出了插件的定义，那插件到底能做什么？</p>
<h3 id="扩展内置对象的接口"><a class="markdown-anchor" href="#扩展内置对象的接口">#</a> 扩展内置对象的接口</h3>
<p>在插件相应的文件内对框架内置对象进行扩展，和应用一样</p>
<ul>
<li><code>app/extend/request.js</code> - 扩展 Koa#Request 类</li>
<li><code>app/extend/response.js</code> - 扩展 Koa#Response 类</li>
<li><code>app/extend/context.js</code> - 扩展 Koa#Context 类</li>
<li><code>app/extend/helper.js</code> - 扩展 Helper 类</li>
<li><code>app/extend/application.js</code> - 扩展 Application 类</li>
<li><code>app/extend/agent.js</code> - 扩展 Agent 类</li>
</ul>
<h3 id="插入自定义中间件"><a class="markdown-anchor" href="#插入自定义中间件">#</a> 插入自定义中间件</h3>
<ol>
<li>
<p>首先在 <code>app/middleware</code> 目录下定义好中间件实现</p>
 <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="meta">'use strict'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> staticCache = <span class="built_in">require</span>(<span class="string">'koa-static-cache'</span>);</span><br><span class="line"><span class="keyword">const</span> assert = <span class="built_in">require</span>(<span class="string">'assert'</span>);</span><br><span class="line"><span class="keyword">const</span> mkdirp = <span class="built_in">require</span>(<span class="string">'mkdirp'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">options, app</span>) =&gt;</span> &#123;</span><br><span class="line">  assert.strictEqual(<span class="keyword">typeof</span> options.dir, <span class="string">'string'</span>, <span class="string">'Must set `app.config.static.dir` when static plugin enable'</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ensure directory exists</span></span><br><span class="line">  mkdirp.sync(options.dir);</span><br><span class="line"></span><br><span class="line">  app.loggers.coreLogger.info(<span class="string">'[egg-static] starting static serve %s -&gt; %s'</span>, options.prefix, options.dir);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> staticCache(options);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</li>
<li>
<p>在 <code>app.js</code> 中将中间件插入到合适的位置（例如：下面将 static 中间件放到 bodyParser 之前）</p>
 <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> assert = <span class="built_in">require</span>(<span class="string">'assert'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 将 static 中间件放到 bodyParser 之前</span></span><br><span class="line">  <span class="keyword">const</span> index = app.config.coreMiddleware.indexOf(<span class="string">'bodyParser'</span>);</span><br><span class="line">  assert(index &gt;= <span class="number">0</span>, <span class="string">'bodyParser 中间件必须存在'</span>);</span><br><span class="line"></span><br><span class="line">  app.config.coreMiddleware.splice(index, <span class="number">0</span>, <span class="string">'static'</span>);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</li>
</ol>
<h3 id="在应用启动时做一些初始化工作"><a class="markdown-anchor" href="#在应用启动时做一些初始化工作">#</a> 在应用启动时做一些初始化工作</h3>
<ul>
<li>
<p>我在启动前想读取一些本地配置</p>
  <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// $&#123;plugin_root&#125;/app.js</span></span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">'fs'</span>);</span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.customData = fs.readFileSync(path.join(app.config.baseDir, <span class="string">'data.bin'</span>));</span><br><span class="line"></span><br><span class="line">  app.coreLogger.info(<span class="string">'read data ok'</span>);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</li>
<li>
<p>如果有异步启动逻辑，可以使用 <code>app.beforeStart</code> API</p>
  <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// $&#123;plugin_root&#125;/app.js</span></span><br><span class="line"><span class="keyword">const</span> MyClient = <span class="built_in">require</span>(<span class="string">'my-client'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.myClient = <span class="keyword">new</span> MyClient();</span><br><span class="line">  app.myClient.on(<span class="string">'error'</span>, err =&gt; &#123;</span><br><span class="line">    app.coreLogger.error(err);</span><br><span class="line">  &#125;);</span><br><span class="line">  app.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> app.myClient.ready();</span><br><span class="line">    app.coreLogger.info(<span class="string">'my client is ready'</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</li>
<li>
<p>也可以添加 agent 启动逻辑，使用 <code>agent.beforeStart</code> API</p>
  <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// $&#123;plugin_root&#125;/agent.js</span></span><br><span class="line"><span class="keyword">const</span> MyClient = <span class="built_in">require</span>(<span class="string">'my-client'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">agent</span> =&gt;</span> &#123;</span><br><span class="line">  agent.myClient = <span class="keyword">new</span> MyClient();</span><br><span class="line">  agent.myClient.on(<span class="string">'error'</span>, err =&gt; &#123;</span><br><span class="line">    agent.coreLogger.error(err);</span><br><span class="line">  &#125;);</span><br><span class="line">  agent.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> agent.myClient.ready();</span><br><span class="line">    agent.coreLogger.info(<span class="string">'my client is ready'</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</li>
</ul>
<h3 id="设置定时任务"><a class="markdown-anchor" href="#设置定时任务">#</a> 设置定时任务</h3>
<ol>
<li>
<p>在 <code>package.json</code> 里设置依赖 schedule 插件</p>
 <figure class="highlight json"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"name"</span>: <span class="string">"your-plugin"</span>,</span><br><span class="line">  <span class="attr">"eggPlugin"</span>: &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"your-plugin"</span>,</span><br><span class="line">    <span class="attr">"dependencies"</span>: [ <span class="string">"schedule"</span> ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
<li>
<p>在 <code>${plugin_root}/app/schedule/</code> 目录下新建文件，编写你的定时任务</p>
 <figure class="highlight js"><table><tr><td class="code"><pre><span class="line">exports.schedule = &#123;</span><br><span class="line">  type: <span class="string">'worker'</span>,</span><br><span class="line">  cron: <span class="string">'0 0 3 * * *'</span>,</span><br><span class="line">  <span class="comment">// interval: '1h',</span></span><br><span class="line">  <span class="comment">// immediate: true,</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">exports.task = <span class="keyword">async</span> ctx =&gt; &#123;</span><br><span class="line">  <span class="comment">// your logic code</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</li>
</ol>
<h3 id="全局实例插件的最佳实践"><a class="markdown-anchor" href="#全局实例插件的最佳实践">#</a> 全局实例插件的最佳实践</h3>
<p>许多插件的目的都是将一些已有的服务引入到框架中，如 <a href="https://github.com/eggjs/egg-mysql" target="_blank" rel="noopener">egg-mysql</a>, <a href="https://github.com/eggjs/egg-oss" target="_blank" rel="noopener">egg-oss</a>。他们都需要在 app 上创建对应的实例。而在开发这一类的插件时，我们发现存在一些普遍性的问题：</p>
<ul>
<li>在一个应用中同时使用同一个服务的不同实例（连接到两个不同的 MySQL 数据库）。</li>
<li>从其他服务获取配置后动态初始化连接（从配置中心获取到 MySQL 服务地址后再建立连接）。</li>
</ul>
<p>如果让插件各自实现，可能会出现各种奇怪的配置方式和初始化方式，所以框架提供了 <code>app.addSingleton(name, creator)</code> 方法来统一这一类服务的创建。需要注意的是在使用 <code>app.addSingleton(name, creator)</code> 方法时，配置文件中一定要有 <code>client</code> 或者 <code>clients</code> 为 key 的配置作为传入 <code>creator</code> 函数 的 <code>config</code>。</p>
<h4 id="插件写法"><a class="markdown-anchor" href="#插件写法">#</a> 插件写法</h4>
<p>我们将 <a href="https://github.com/eggjs/egg-mysql" target="_blank" rel="noopener">egg-mysql</a> 的实现简化之后来看看如何编写此类插件：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// egg-mysql/app.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 第一个参数 mysql 指定了挂载到 app 上的字段，我们可以通过 `app.mysql` 访问到 MySQL singleton 实例</span></span><br><span class="line">  <span class="comment">// 第二个参数 createMysql 接受两个参数(config, app)，并返回一个 MySQL 的实例</span></span><br><span class="line">  app.addSingleton(<span class="string">'mysql'</span>, createMysql);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @param  &#123;Object&#125; config   框架处理之后的配置项，如果应用配置了多个 MySQL 实例，会将每一个配置项分别传入并调用多次 createMysql</span></span><br><span class="line"><span class="comment"> * @param  &#123;Application&#125; app 当前的应用</span></span><br><span class="line"><span class="comment"> * @return &#123;Object&#125;          返回创建的 MySQL 实例</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createMysql</span>(<span class="params">config, app</span>) </span>&#123;</span><br><span class="line">  assert(config.host &amp;&amp; config.port &amp;&amp; config.user &amp;&amp; config.database);</span><br><span class="line">  <span class="comment">// 创建实例</span></span><br><span class="line">  <span class="keyword">const</span> client = <span class="keyword">new</span> Mysql(config);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 做启动应用前的检查</span></span><br><span class="line">  app.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> rows = <span class="keyword">await</span> client.query(<span class="string">'select now() as currentTime;'</span>);</span><br><span class="line">    app.coreLogger.info(<span class="string">`[egg-mysql] init instance success, rds currentTime: <span class="subst">$&#123;rows[<span class="number">0</span>].currentTime&#125;</span>`</span>);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> client;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>初始化方法也支持 <code>Async function</code>，便于有些特殊的插件需要异步化获取一些配置文件：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">createMysql</span>(<span class="params">config, app</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 异步获取 mysql 配置</span></span><br><span class="line">  <span class="keyword">const</span> mysqlConfig = <span class="keyword">await</span> app.configManager.getMysqlConfig(config.mysql);</span><br><span class="line">  assert(mysqlConfig.host &amp;&amp; mysqlConfig.port &amp;&amp; mysqlConfig.user &amp;&amp; mysqlConfig.database);</span><br><span class="line">  <span class="comment">// 创建实例</span></span><br><span class="line">  <span class="keyword">const</span> client = <span class="keyword">new</span> Mysql(mysqlConfig);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 做启动应用前的检查</span></span><br><span class="line">  <span class="keyword">const</span> rows = <span class="keyword">await</span> client.query(<span class="string">'select now() as currentTime;'</span>);</span><br><span class="line">  app.coreLogger.info(<span class="string">`[egg-mysql] init instance success, rds currentTime: <span class="subst">$&#123;rows[<span class="number">0</span>].currentTime&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> client;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>可以看到，插件中我们只需要提供要挂载的字段以及对应服务的初始化方法，所有的配置管理、实例获取方式都由框架封装并统一提供了。</p>
<h4 id="应用层使用方案"><a class="markdown-anchor" href="#应用层使用方案">#</a> 应用层使用方案</h4>
<h5 id="单实例"><a class="markdown-anchor" href="#单实例">#</a> 单实例</h5>
<ol>
<li>
<p>在配置文件中声明 MySQL 的配置。</p>
 <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/config.default.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  mysql: &#123;</span><br><span class="line">    client: &#123;</span><br><span class="line">      host: <span class="string">'mysql.com'</span>,</span><br><span class="line">      port: <span class="string">'3306'</span>,</span><br><span class="line">      user: <span class="string">'test_user'</span>,</span><br><span class="line">      password: <span class="string">'test_password'</span>,</span><br><span class="line">      database: <span class="string">'test'</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</li>
<li>
<p>直接通过 <code>app.mysql</code> 访问数据库。</p>
 <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/controller/post.js</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> list() &#123;</span><br><span class="line">    <span class="keyword">const</span> posts = <span class="keyword">await</span> <span class="keyword">this</span>.app.mysql.query(sql, values);</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
</ol>
<h5 id="多实例"><a class="markdown-anchor" href="#多实例">#</a> 多实例</h5>
<ol>
<li>
<p>同样需要在配置文件中声明 MySQL 的配置，不过和单实例时不同，配置项中需要有一个 <code>clients</code> 字段，分别申明不同实例的配置，同时可以通过 <code>default</code> 字段来配置多个实例中共享的配置（如 host 和 port）。需要注意的是在这种情况下要用 <code>get</code> 方法指定相应的实例。（例如：使用 <code>app.mysql.get('db1').query()</code>，而不是直接使用 <code>app.mysql.query()</code> 得到一个 <code>undefined</code>）。</p>
 <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/config.default.js</span></span><br><span class="line">exports.mysql = &#123;</span><br><span class="line">  clients: &#123;</span><br><span class="line">    <span class="comment">// clientId, access the client instance by app.mysql.get('clientId')</span></span><br><span class="line">    db1: &#123;</span><br><span class="line">      user: <span class="string">'user1'</span>,</span><br><span class="line">      password: <span class="string">'upassword1'</span>,</span><br><span class="line">      database: <span class="string">'db1'</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    db2: &#123;</span><br><span class="line">      user: <span class="string">'user2'</span>,</span><br><span class="line">      password: <span class="string">'upassword2'</span>,</span><br><span class="line">      database: <span class="string">'db2'</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// default configuration for all databases</span></span><br><span class="line">  <span class="keyword">default</span>: &#123;</span><br><span class="line">    host: <span class="string">'mysql.com'</span>,</span><br><span class="line">    port: <span class="string">'3306'</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</li>
<li>
<p>通过 <code>app.mysql.get('db1')</code> 来获取对应的实例并使用。</p>
 <figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/controller/post.js</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> list() &#123;</span><br><span class="line">    <span class="keyword">const</span> posts = <span class="keyword">await</span> <span class="keyword">this</span>.app.mysql.get(<span class="string">'db1'</span>).query(sql, values);</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
</ol>
<h5 id="动态创建实例"><a class="markdown-anchor" href="#动态创建实例">#</a> 动态创建实例</h5>
<p>我们可以不需要将配置提前申明在配置文件中，而是在应用运行时动态的初始化一个实例。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="comment">// 从配置中心获取 MySQL 的配置 &#123; host, post, password, ... &#125;</span></span><br><span class="line">    <span class="keyword">const</span> mysqlConfig = <span class="keyword">await</span> app.configCenter.fetch(<span class="string">'mysql'</span>);</span><br><span class="line">    <span class="comment">// 动态创建 MySQL 实例</span></span><br><span class="line">    app.database = <span class="keyword">await</span> app.mysql.createInstanceAsync(mysqlConfig);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>通过 <code>app.database</code> 来使用这个实例。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/controller/post.js</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> list() &#123;</span><br><span class="line">    <span class="keyword">const</span> posts = <span class="keyword">await</span> <span class="keyword">this</span>.app.database.query(sql, values);</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><strong>注意，在动态创建实例的时候，框架也会读取配置中 <code>default</code> 字段内的配置项作为默认配置。</strong></p>
<h3 id="插件的寻址规则"><a class="markdown-anchor" href="#插件的寻址规则">#</a> 插件的寻址规则</h3>
<p>框架在加载插件的时候，遵循下面的寻址规则：</p>
<ul>
<li>
<p>如果配置了 path，直接按照 path 加载</p>
</li>
<li>
<p>没有 path 根据 package 名去查找，查找的顺序依次是</p>
<ol>
<li>应用根目录下的 <code>node_modules</code></li>
<li>应用依赖框架路径下的 <code>node_modules</code></li>
<li>当前路径下的 <code>node_modules</code> （主要是兼容单元测试场景）</li>
</ol>
</li>
</ul>
<h3 id="插件规范"><a class="markdown-anchor" href="#插件规范">#</a> 插件规范</h3>
<p>我们非常欢迎您贡献新的插件，同时也希望您遵守下面一些规范：</p>
<ul>
<li>命名规范
<ul>
<li><code>npm</code> 包名以 <code>egg-</code> 开头，且为全小写，例如：<code>egg-xx</code>。比较长的词组用中划线：<code>egg-foo-bar</code></li>
<li>对应的插件名使用小驼峰，小驼峰转换规则以 <code>npm</code> 包名的中划线为准 <code>egg-foo-bar</code> =&gt; <code>fooBar</code></li>
<li>对于可以中划线也可以不用的情况，不做强制约定，例如：userservice(egg-userservice) 还是 user-service(egg-user-service) 都可以</li>
</ul>
</li>
<li><code>package.json</code> 书写规范
<ul>
<li>
<p>按照上面的文档添加 <code>eggPlugin</code> 节点</p>
</li>
<li>
<p>在 <code>keywords</code> 里加上 <code>egg</code>、<code>egg-plugin</code>、<code>eggPlugin</code> 等关键字，便于索引</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"name"</span>: <span class="string">"egg-view-nunjucks"</span>,</span><br><span class="line">  <span class="attr">"version"</span>: <span class="string">"1.0.0"</span>,</span><br><span class="line">  <span class="attr">"description"</span>: <span class="string">"view plugin for egg"</span>,</span><br><span class="line">  <span class="attr">"eggPlugin"</span>: &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"nunjucks"</span>,</span><br><span class="line">    <span class="attr">"dep"</span>: [</span><br><span class="line">      <span class="string">"security"</span></span><br><span class="line">    ]</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"keywords"</span>: [</span><br><span class="line">    <span class="string">"egg"</span>,</span><br><span class="line">    <span class="string">"egg-plugin"</span>,</span><br><span class="line">    <span class="string">"eggPlugin"</span>,</span><br><span class="line">    <span class="string">"egg-plugin-view"</span>,</span><br><span class="line">    <span class="string">"egg-view"</span>,</span><br><span class="line">    <span class="string">"nunjucks"</span></span><br><span class="line">  ],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
</ul>
</li>
</ul>
<h2 id="为何不使用-npm-包名来做插件名"><a class="markdown-anchor" href="#为何不使用-npm-包名来做插件名">#</a> 为何不使用 npm 包名来做插件名？</h2>
<p>Egg 是通过 <code>eggPlugin.name</code> 来定义插件名的，只在应用或框架具备唯一性，也就是说<strong>多个 npm 包可能有相同的插件名</strong>，为什么这么设计呢？</p>
<p>首先 Egg 插件不仅仅支持 npm 包，还支持通过目录来找插件。在<a href="../tutorials/progressive.html">渐进式开发</a>章节提到如何使用这两个配置来进行代码演进。目录对单元测试也比较友好。所以 Egg 无法通过 npm 的包名来做唯一性。</p>
<p>更重要的是 Egg 可以使用这种特性来做适配器。比如<a href="./view-plugin.html#%E6%8F%92%E4%BB%B6%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83">模板开发规范</a>定义的插件名为 view，而存在 <code>egg-view-nunjucks</code>，<code>egg-view-react</code> 等插件，使用者只需要更换插件和修改模板，不需要动 Controller， 因为所有的模板插件都实现了相同的 API。</p>
<p><strong>将相同功能的插件赋予相同的插件名，具备相同的 API，可以快速切换</strong>。这在模板、数据库等领域非常适用。</p>

  </article>
  <aside id="mobileAside" class="toc">
  <div class="mobile-menu">
    <ul>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/advanced/plugin.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/advanced/plugin.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
    </ul>
  </div>
  <dl><dt id="title-Intro" style="cursor: pointer;" class="aside-title">新手指南<a id="collapse-icon-Intro" class="icon opend"></a></dt><dd id=panel-Intro><ul><li><a href="/zh-cn/intro/index.html" class="menu-link">Egg.js 是什么?</a></li><li><a href="/zh-cn/intro/egg-and-koa.html" class="menu-link">Egg.js 和 Koa</a></li><li><a href="/zh-cn/intro/quickstart.html" class="menu-link">快速入门</a></li><li><a href="/zh-cn/tutorials/progressive.html" class="menu-link">渐进式开发</a></li><li><a href="/zh-cn/migration.html" class="menu-link">2.x 升级指南</a></li></ul></dd><dt id="title-Basics" style="cursor: pointer;" class="aside-title">基础功能<a id="collapse-icon-Basics" class="icon opend"></a></dt><dd id=panel-Basics><ul><li><a href="/zh-cn/basics/structure.html" class="menu-link">目录结构</a></li><li><a href="/zh-cn/basics/objects.html" class="menu-link">内置对象</a></li><li><a href="/zh-cn/basics/env.html" class="menu-link">运行环境</a></li><li><a href="/zh-cn/basics/config.html" class="menu-link">配置</a></li><li><a href="/zh-cn/basics/middleware.html" class="menu-link">中间件</a></li><li><a href="/zh-cn/basics/router.html" class="menu-link">Router</a></li><li><a href="/zh-cn/basics/controller.html" class="menu-link">Controller</a></li><li><a href="/zh-cn/basics/service.html" class="menu-link">Service</a></li><li><a href="/zh-cn/basics/plugin.html" class="menu-link">插件</a></li><li><a href="/zh-cn/basics/schedule.html" class="menu-link">定时任务</a></li><li><a href="/zh-cn/basics/extend.html" class="menu-link">框架扩展</a></li><li><a href="/zh-cn/basics/app-start.html" class="menu-link">启动自定义</a></li></ul></dd><dt id="title-Core" style="cursor: pointer;" class="aside-title">核心功能<a id="collapse-icon-Core" class="icon opend"></a></dt><dd id=panel-Core><ul><li><a href="/zh-cn/core/development.html" class="menu-link">本地开发</a></li><li><a href="/zh-cn/core/unittest.html" class="menu-link">单元测试</a></li><li><a href="/zh-cn/core/deployment.html" class="menu-link">应用部署</a></li><li><a href="/zh-cn/core/logger.html" class="menu-link">日志</a></li><li><a href="/zh-cn/core/httpclient.html" class="menu-link">HttpClient</a></li><li><a href="/zh-cn/core/cookie-and-session.html" class="menu-link">Cookie and Session</a></li><li><a href="/zh-cn/core/cluster-and-ipc.html" class="menu-link">多进程模型和进程间通讯</a></li><li><a href="/zh-cn/core/view.html" class="menu-link">模板渲染</a></li><li><a href="/zh-cn/core/error-handling.html" class="menu-link">异常处理</a></li><li><a href="/zh-cn/core/security.html" class="menu-link">安全</a></li><li><a href="/zh-cn/core/i18n.html" class="menu-link">国际化</a></li></ul></dd><dt id="title-Tutorials" style="cursor: pointer;" class="aside-title">教程<a id="collapse-icon-Tutorials" class="icon opend"></a></dt><dd id=panel-Tutorials><ul><li><a href="/zh-cn/tutorials/mysql.html" class="menu-link">MySQL</a></li><li><a href="/zh-cn/tutorials/restful.html" class="menu-link">RESTful API</a></li><li><a href="/zh-cn/tutorials/passport.html" class="menu-link">Passport 鉴权</a></li><li><a href="/zh-cn/tutorials/socketio.html" class="menu-link">Socket.IO</a></li><li><a href="/zh-cn/tutorials/assets.html" class="menu-link">静态资源</a></li><li><a href="/zh-cn/tutorials/typescript.html" class="menu-link">TypeScript</a></li></ul></dd><dt id="title-Advanced" style="cursor: pointer;" class="aside-title">进阶<a id="collapse-icon-Advanced" class="icon opend"></a></dt><dd id=panel-Advanced><ul><li><a href="/zh-cn/advanced/loader.html" class="menu-link">Loader</a></li><li><a href="/zh-cn/advanced/plugin.html" class="menu-link">插件开发</a></li><li><a href="/zh-cn/advanced/framework.html" class="menu-link">框架开发</a></li><li><a href="/zh-cn/advanced/cluster-client.html" class="menu-link">多进程研发模式增强</a></li><li><a href="/zh-cn/advanced/view-plugin.html" class="menu-link">模板插件开发规范</a></li><li><a href="/zh-cn/style-guide.html" class="menu-link">代码风格指南</a></li></ul></dd><dt id="title-Community" style="cursor: pointer;" class="aside-title">社区<a id="collapse-icon-Community" class="icon opend"></a></dt><dd id=panel-Community><ul><li><a href="/zh-cn/plugins/" class="menu-link">内置插件列表</a></li><li><a href="/zh-cn/contributing.html" class="menu-link">如何贡献</a></li><li><a href="/zh-cn/resource.html" class="menu-link">资源</a></li><li><a href="/zh-cn/faq.html" class="menu-link">常见问题</a></li></ul></dd></dl>
</aside>
<script>
var mobileTrigger = document.getElementById('mobileTrigger');
var mobileAside = document.getElementById('mobileAside');

var expandMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon opend';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = '';
  }
}

var collapseMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon closed';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = 'aside-panel-hidden';
  }
}

mobileAside.onclick = function(e) {
  const targetId = e.target.id;
  if (targetId && (targetId.indexOf('title-') > -1 || targetId.indexOf('collapse-icon-') > -1)) {
    const title = targetId.replace('title-', '').replace('collapse-icon-', '');
    try { 
      // the the browser may have no localStroage or JSON.parse may throw exception.
      const menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
        
      // current menu status
      const curClosed = menuInfo[title] ? menuInfo[title].closed : false; // default false

      // change UI
      curClosed ? expandMenu(title) : collapseMenu(title);

      // save menuInfo to localStorage
      menuInfo[title] = { closed: !curClosed } // opposite
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    } catch (e) {}
  }
};

mobileTrigger.onclick = function(e) {
  e.preventDefault();
  if (mobileAside.className.indexOf('mobile-show') === -1) {
    mobileAside.className += ' mobile-show';
  } else {
    mobileAside.className = 'toc';
  }
};

(function() {
  // save data to localStorage because the page will refresh when user change the url.
  let menuInfo;
  try { 
    // the the browser may have no localStroage or JSON.parse may throw exception.
    menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
    if (!menuInfo) {
      menuInfo = {};
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    }
  } catch (e) {
    menuInfo = {}; // default {}
  }

  for (const title in menuInfo) {
    if (menuInfo[title] && menuInfo[title].closed) { // menu in closed status.
      collapseMenu(title);
    } else {
      expandMenu(title);
    }
  }

  // highlight menu
  const pathname = window.location.pathname;
  const selector = `a[href="${pathname}"].menu-link,a[href="${pathname}index.html"].menu-link`;
  const menuItem = mobileAside.querySelector(selector);
  if (menuItem) { menuItem.className += ' highlight'; }
})();
</script>

</div>

  </div>
</body>
<script src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js"></script>
<script>
docsearch({
  apiKey: '1561de31a86f79507ea00cdb54ce647c',
  indexName: 'eggjs',
  inputSelector: '#search-query',
});
</script>
<div class="cnzz">
<script src="https://s11.cnzz.com/z_stat.php?id=1261142226&web_id=1261142226" language="JavaScript"></script>
</div>

</html>
